# 什么是抽象

举个例子，我吃完饭在大街上散步，走得有点儿累了，于是对自己说：​“腿真疼啊，找把椅子坐吧。​”此时此刻，​“椅子”在我脑中就是一个抽象的概念。我脑中的椅子：

- 有一个平坦的表面可以把屁股放上去；
- 离地20到50厘米，能支撑60千克以上的重量。

对这个抽象概念来说，路边的金属黑色长椅是我需要的椅子，饭店门口的塑料扶手椅同样也是我需要的椅子，甚至某个一尘不染的台阶也可以成为我要的“椅子”​。

# 软件开发中的抽象

计算机网络7层OSI模型：物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。

在OSI模型的第一层物理层，主要关注原始字节流如何通过物理媒介传输，牵涉针脚、集线器等各种细节。而第七层应用层则更贴近终端用户，这层包含的都是我们日常用到的东西，比如浏览网页的HTTP协议、发送邮件的SMTP协议，等等。

在这种分层结构下，每一层抽象都只依赖比它抽象级别更低的层，同时对比它抽象级别更高的层一无所知。因此，每层都可以脱离更高级别的层独立工作。比如活跃在传输层的TCP协议，可以对应用层的HTTP、HTTPS等应用协议毫无感知，独立工作。

# 函数中的抽象

下面我们将实现“调用API查找歌手的第一张专辑”的功能。

```python
"""通过 iTunes API 搜索歌手发布的第一张专辑"""
import sys
from json.decoder import JSONDecodeError
import requests
from requests.exceptions import HTTPError
ITUNES_API_ENDPOINT = 'https://itunes.apple.com/search'

def command_first_album():
    """通过脚本输入查找并打印歌手的第一张专辑信息"""
    if not len(sys.argv) == 2:
        print(f'usage: python {sys.argv[0]} {{SEARCH_TERM}}')
        sys.exit（1）
    term = sys.argv[1]
    resp = requests.get(
        ITUNES_API_ENDPOINT,
        {
            'term': term,
            'media': 'music',
            'entity': 'album',
            'attribute': 'artistTerm',
            'limit': 200,
        },
    )
    try:
        resp.raise_for_status()
    except HTTPError as e:
        print(f'Error: failed to call iTunes API, {e}')
        sys.exit（2） ➊
    try:
        albums = resp.json()['results']
    except JSONDecodeError:
        print(f'Error: response is not valid JSON format')
        sys.exit（2）
    if not albums:
        print(f'Error: no albums found for artist "{term}"')
        sys.exit（1）
    sorted_albums = sorted(albums, key=lambda item: item['releaseDate'])
    first_album = sorted_albums[0]
    # 去除发布日期里的小时与分钟信息
    release_date = first_album['releaseDate'].split('T')[0]
    # 打印结果
    print(f"{term}'s first album: ")
    print(f" * Name: {first_album['collectionName']}")
    print(f" * Genre: {first_album['primaryGenreName']}")
    print(f" * Released at: {release_date}")

if __name__== '__main__':
    command_first_album()
```

这个脚本实现了我们想要的效果，那么它的代码质量怎么样呢？我们从长度、圈复杂度、嵌套层级几个维度来看看：

1. 主函数command_first_album()共40行代码；
2. 函数圈复杂度为5；
3. 函数内最大嵌套层级为1。

看上去每个维度都在合理范围内，没有什么问题。但是，除了上面这些维度外，评价函数好坏还有一个重要标准：函数内的代码是否在同一个抽象层内。

在我看来，这个程序至少可以分为以下三层。

1. 用户界面层：处理用户输入、输出结果。
2. “第一张专辑”层：找到第一张专辑。
3. 专辑数据层：调用API获取专辑信息。

重构后的代码如下：

```python
""" 通过 iTunes API 搜索歌手发布的第一张专辑"""
import sys
from json.decoder import JSONDecodeError
import requests
from requests.exceptions import HTTPError
ITUNES_API_ENDPOINT = 'https://itunes.apple.com/search'

class GetFirstAlbumError(Exception):
    """ 获取第一张专辑失败"""
class QueryAlbumsError(Exception):
    """获取专辑列表失败"""

def command_first_album():
    """通过输入参数查找并打印歌手的第一张专辑信息"""
    if not len(sys.argv) == 2:
        print(f'usage: python {sys.argv[0]} {{SEARCH_TERM}}')
        sys.exit（1）
    artist = sys.argv[1]
    try:
        album = get_first_album(artist)
    except GetFirstAlbumError as e:
        print(f"error: {e}", file=sys.stderr)
        sys.exit（2）
    print(f"{artist}'s first album: ")
    print(f" * Name: {album['name']}")
    print(f" * Genre: {album['genre_name']}")
    print(f" * Released at: {album['release_date']}")

def get_first_album(artist):
    """根据专辑列表获取第一张专辑
    :param artist: 歌手名字
    :return: 第一张专辑
    :raises: 获取失败时抛出 GetFirstAlbumError
    """
    try:
        albums = query_all_albums(artist)
    except QueryAlbumsError as e:
        raise GetFirstAlbumError(str(e))
    sorted_albums = sorted(albums, key=lambda item: item['releaseDate'])
    first_album = sorted_albums[0]
    # 去除发布日期里的小时与分钟信息
    release_date = first_album['releaseDate'].split('T')[0]
    return {
        'name': first_album['collectionName'],
        'genre_name': first_album['primaryGenreName'],
        'release_date': release_date,
    }

def query_all_albums(artist):
    """根据歌手名字搜索所有专辑列表
    :param artist: 歌手名字
    :return: 专辑列表，List[Dict]
    :raises: 获取专辑失败时抛出 GetAlbumsError
    """
    resp = requests.get(
        ITUNES_API_ENDPOINT,
        {
            'term': artist,
            'media': 'music',
            'entity': 'album',
            'attribute': 'artistTerm',
            'limit': 200,
        },
    )
    try:
        resp.raise_for_status()
    except HTTPError as e:
        raise QueryAlbumsError(f'failed to call iTunes API, {e}')
    try:
        albums = resp.json()['results']
    except JSONDecodeError:
        raise QueryAlbumsError('response is not valid JSON format')
    if not albums:
        raise QueryAlbumsError(f'no albums found for artist "{artist}"')
    return albums
if __name__== '__main__':
    command_first_album()
```

经过调整后，脚本里每个函数内的所有代码都只属于同一个抽象层。这大大提升了函数代码的说明性。现在，当你在阅读每个函数时，可以很清晰地知道它在做什么事情。

# 未完待续……
